package org.chorus;

import java.awt.Point;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Map;
import java.util.TreeMap;

import org.gicentre.utils.stat.AbstractChart;
import org.gicentre.utils.stat.XYChart;

import jwo.landserf.structure.RasterMap;
import jwo.landserf.structure.SpatialModel;

import processing.core.PApplet;
import processing.core.PConstants;
import processing.core.PGraphics;
import processing.core.PVector;

//  **********************************************************************************
/** Module for creating and viewing inclination profiles generated from a DEM.
 *  @author Jo Wood
 *  @version 1.3.2, 22nd March, 2013
 */
//  **********************************************************************************

/* This file is part of the Chorus river bed analysis software package. Chorus is free software:
 * you can redistribute it and/or modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 * 
 * Chorus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this
 * source code (see COPYING included with this source code). If not, see 
 * http://www.gnu.org/licenses/.
 */

public class InclinationChart implements Modular,Comparable<Modular>
{
	// ----------------------------- Object variables ------------------------------

	private PApplet parent;					// Parent sketch in which this class is embedded.
	private PGraphics graphics;				// Graphics context for output.
	private ArrayList<XYChart> charts;		// Graphical representation of the inclination curves.
	private ArrayList<String> titles;		// Titles of DEMs from which to calculate the inclination curves.
	private int order;						// Order number for sorted modules.
	private String units;					// Planimetric units to be labelled on the chart x-axis.
	private Direction direction;			// Direction in which the inclination chart is calculated.
	private Direction downstreamDir;		// Downstream direction of the DEM.
	private int colourOffset;				// Allows arbitrary colouring of curve.
	private float maxLag;					// Maximum lag separation.
	
	private static DecimalFormat D_FORMATTER = new DecimalFormat( "##0.000000" );
			
	// ------------------------------- Constructors ---------------------------------
	
	/** Creates the inclination curve. This version will calculate inclinations in the downstream direction 
	 *  up to a maximum lag of the smaller side of the surface being measured assuming the downstream direction
	 *  is from left to right.
	 */
	public InclinationChart()
	{
		this(Direction.DOWNSTREAM, Direction.LEFT_TO_RIGHT,0);
	}
	
	/** Creates the inclination curve. This version will calculate lags in the downstream direction, assuming
	 *  downstream is left to right up to the given maximum lag.
	 *  @param maxLag Maximum lag in grid cell counts or 0 if half the short side of the surface.
	 */
	public InclinationChart(float maxLag)
	{
		this(Direction.DOWNSTREAM, Direction.LEFT_TO_RIGHT,maxLag);
	}
	
	/** Creates the inclination using lags in the given direction. Maximum permitted lag will be half the
	 *  shorter side of the surface being measured.
	 *  @param direction Direction along which to calculate inclinations. This can either be DOWNSTREAM or CROSSSTREAM.
	 *  @param downstreamDir Downstream direction in raster. This can be either LEFT_TO_RIGHT or TOP_TO_BOTTOM.
	 */
	public InclinationChart(Direction direction, Direction downstreamDir)
	{
		this(direction,downstreamDir,0);
	}
	
	/** Creates the inclination using lags in the given direction.
	 *  @param direction Direction along which to calculate inclinations. This can either be DOWNSTREAM or CROSSSTREAM.
	 *  @param downstreamDir Downstream direction in raster. This can be either LEFT_TO_RIGHT or TOP_TO_BOTTOM.
	 *  @param maxLag Maximum lag in grid cell counts or 0 if half the short side of the surface.
	 */
	public InclinationChart(Direction direction, Direction downstreamDir, float maxLag)
	{
		this.direction = direction;
		this.downstreamDir = downstreamDir;
		this.maxLag = maxLag;
	}
		
	// ---------------------------------- Methods ----------------------------------

	/** Initialises the inclination module.
	 */
	public void setup(PApplet parent)
	{
		this.parent = parent;
		titles = new ArrayList<String>();
		charts = new ArrayList<XYChart>();
		setGraphics(parent.g);
		colourOffset = 0;
		units = "mm";
	}
	
	/** Sets the the graphics context into which all output is directed. This method allows
	 *  output to be redirected to print output, offscreen buffers etc.
	 *  @param graphics New graphics context in which the modular class is embedded.
	 */
	public void setGraphics(PGraphics graphics)
	{
		this.graphics = graphics;
		
		for (AbstractChart chart : charts)
		{
			chart.setGraphics(graphics);
		}
	}
	
	/** Sets the order number for this module. Allows a list of modules to be sorted in some arbitrary order.
	 *  @param order Order number where the smaller the number, the earlier in a sorted list the module will appear.
	 */
	public void setOrder(int order)
	{
		this.order = order;
	}
	
	/** Reports the order number for this module. Allows a list of modules to be sorted in some arbitrary order.
	 *  @return Order number where the smaller the number, the earlier in a sorted list the module will appear.
	 */
	public int getOrder()
	{
		return order;
	}
	
	/** Sets the units labels to be displayed on the horizontal axis of the inclination chart.
	 *  @param units Units labels (e.g. 'mm' or 'cm').
	 */
	public void setUnits(String units)
	{
		this.units = units;
		
		for (XYChart chart : charts)
		{
			switch (direction)
			{
				case CROSSSTREAM:
					chart.setXAxisLabel("Cross-stream Lag ("+units+")");
					break;
				case DOWNSTREAM:
					chart.setXAxisLabel("Downstream Lag ("+units+")");
					break;
				default:
					chart.setXAxisLabel("Lag (all directions, ("+units+")");
					break;
			}
		}
	}

	/** Draws the inclination chart.
	 *  @param xOrigin Left-hand pixel coordinate of the area in which to draw graph.
	 *  @param yOrigin Top pixel coordinate of the area in which to draw graph.
	 *  @param width Width in pixels of the area in which to draw graph.
	 *  @param height Height in pixels of the area in which to draw graph.
	 */
	public void draw(float xOrigin, float yOrigin, float width, float height)
	{
		// Check to see that we have at least one DEM to display		
		if (charts.size() == 0)
		{
			graphics.fill(graphics.color(80));
			graphics.textFont(DisplayUtils.mediumFont);
			graphics.textAlign(PConstants.CENTER, PConstants.CENTER);
			graphics.text("No DEMs available for inclination chart",xOrigin+width/2,yOrigin+height/2);
			return;
		}

		PVector blPos=null, trPos = null;
		
		// Maintain a scale with respect to enclosing display area (stops text and lines thickness being to large).
		float graphScale = width/400;
		
		graphics.pushMatrix();
		graphics.translate(xOrigin,yOrigin);
		graphics.scale(graphScale,graphScale);
		graphics.translate(-xOrigin, -yOrigin);

		for (int i=0; i<charts.size(); i++)
		{
			// Draw inclination curve.
			XYChart chart = charts.get(i);
			
			int chartColour;
			if (charts.size() == 1)
			{
				chartColour = parent.color(0);
			}
			else
			{
				chartColour = DisplayUtils.getCategoryPalette().findColour(colourOffset+i);
			}
			
			chart.setLineColour(parent.color(chartColour,100));
			chart.setPointColour(chartColour);
			
			graphics.textFont(DisplayUtils.smallFont);
			chart.draw(xOrigin, yOrigin, width/graphScale,height/graphScale);

			blPos = charts.get(0).getDataToScreen(new PVector(charts.get(0).getMinX(),0));
			trPos = charts.get(0).getDataToScreen(new PVector(charts.get(0).getMaxX(),charts.get(0).getMaxY()));

			// Show title of raster for which inclination is displayed.
			graphics.fill(chartColour);
			graphics.textFont(DisplayUtils.smallFont);
graphics.textSize(6);
			graphics.textAlign(PConstants.LEFT,PConstants.TOP);
			float textHeight = graphics.textAscent()+graphics.textDescent()+2;
			graphics.text(titles.get(i),blPos.x+5,trPos.y+ i*textHeight);
		}

		// Draw 0 inclination line.
		PVector origin = charts.get(0).getDataToScreen(new PVector(0,0));
		PVector rhs    = charts.get(0).getDataToScreen(new PVector(charts.get(0).getMaxX(),0));
		graphics.stroke(0,120);
		graphics.strokeWeight(0.5f);
		graphics.line(origin.x,origin.y,rhs.x,rhs.y);
				
		graphics.popMatrix();
	}
	
	
	/** Adds the given DEM to those whose inclination curves are displayed.
	 *  @param dem DEM to add.
	 */
	public void addDEM(RasterMap dem) 
	{
		if (dem != null)
		{
			buildIncinationCurve(dem);
			titles.add(dem.getHeader().getTitle());
		}
	}
	
	/** Sets the colour palette start colour for display.
	 *  @param offset Position in default colour palette in which the first inclination curve is displayed.
	 */
	public void setColourOffset(int offset)
	{
		this.colourOffset = offset;
	}
	
	/** Sets the maximum inclination value to be shown on the inclination curve
	 *  @param maxInclination Maximum inclination.
	 */
	public void setMaxInclination(float maxInclination)
	{
		for (XYChart chart : charts)
		{
			chart.setMaxY(maxInclination);
			chart.transposeAxes(false);
		}
	}
	
	/** Sets the minimum inclination value to be shown on the inclination curve
	 *  @param minInclination Minimum inclination.
	 */
	public void setMinInclination(float minInclination)
	{
		for (XYChart chart : charts)
		{
			chart.setMinY(minInclination);
			chart.transposeAxes(false);
		}
	}
	
	/** Reports the maximum inclination value of the curve
	 *  @return Maximum inclination shown by the inclination curve
	 */
	public float getMaxInclination()
	{
		float maxIncl = -Float.MAX_VALUE;
		
		for (XYChart chart : charts)
		{
			maxIncl = Math.max(maxIncl,chart.getMaxY());
		}
		return maxIncl;
	}
	
	/** Reports the minimum inclination value of the curve
	 *  @return Minimum inclination shown by the inclination curve
	 */
	public float getMinInclination()
	{
		float minIncl = Float.MAX_VALUE;
		
		for (XYChart chart : charts)
		{
			minIncl = Math.min(minIncl,chart.getMinY());
		}
		return minIncl;
	}
	
	/** Writes the the lag-inclination values out to the given file.
	 *  @param fileName Name of output file.
	 *  @return True if output is written successfully.
	 */
	public boolean writeOutput(String fileName)
	{
		try
		{
			PrintWriter output = new PrintWriter(fileName);
			ArrayList<float[]>inclinations = new ArrayList<float[]>();
			float[] lags = null;
			StringBuffer titleText = new StringBuffer();

			for (int i=0; i<charts.size(); i++)
			{
				if (i==0)
				{
					titleText.append("inclination("+titles.get(i)+")");
				}
				else
				{
					titleText.append(", inclination("+titles.get(i)+")");
				}
				
				// Get Inclination data.
				XYChart chart = charts.get(i);

				if (lags == null)
				{
					lags = chart.getXData();
				}
				else if (lags.length != chart.getXData().length)
				{
					System.err.println("Warning: Inclination values have different lags");
				}
				inclinations.add(chart.getYData());
			}
			if (lags == null)
			{
				System.err.println("Problem extracting inclination values.");
				output.close();
				return false;
			}
			
			output.println("# Lag, "+titleText.toString());
			
			for (int i=0; i<lags.length; i++)
			{
				output.print(lags[i]);
				for (float[] inclination : inclinations)
				{
					output.print(","+D_FORMATTER.format(inclination[i]));
				}
				output.println();
			}

			output.flush();
			output.close();
			System.out.println("Inclination values written to "+fileName);
		}
		catch (IOException e)
		{
			System.err.println("Problem writing inclination output to "+fileName+"': "+e);
			return false;
		}
		return true;
	}

	// ------------------------ Mouse and keyboard handling ------------------------
	
	/** Activates the guide display when the mouse is pressed.
	 */
	public void mousePressed()
	{
		// Do nothing for the moment.
	}

	/** Deactivates the guide display when the mouse is released.
	 */
	public void mouseReleased()
	{
		// Do nothing for the moment.
	}

	/** Updates the guide display as the mouse is dragged.
	 */
	public void mouseDragged()
	{
		// Do nothing in this case.
	}
	
	/** Allows this module to be compared with another to determine sort order.
	 *  @param otherModule Other module to compare with this one.
	 */
	public int compareTo(Modular otherModule)
	{
		int comparison = order-otherModule.getOrder();
		if (comparison == 0)
		{
			return hashCode()-otherModule.hashCode();
		}
		return comparison;
	}

	// ------------------------------ Private methods ------------------------------

	/** Creates a 1D inclination curve from a random sample of raster cell pairs.
	 *  @param dem DEM from which to calculate inclination.
	 */
	private void buildIncinationCurve(RasterMap dem)
	{		
		float maxGlobalLag = 0;
		float maxGlobalIncl = 0;
		
		float maxLagCell = Math.min(dem.getNumRows()/2,dem.getNumCols()/2);
		float gridRes = (dem.getXRes()+dem.getYRes())/2f;
		
		// Limit the maximum lag by parameter if specified or by the maximum permitted by dimensions of the raster
		if (maxLag > 0) 
		{
			if (direction == Direction.ALL)
			{
				maxLagCell = Math.min(maxLag/gridRes, Math.min(dem.getNumRows()/2,dem.getNumCols()/2));
			}
			else if (direction == Direction.DOWNSTREAM)
			{
				if (downstreamDir==Direction.LEFT_TO_RIGHT)
				{
					maxLagCell = Math.min(maxLag/gridRes,dem.getNumCols()/2);
				}
				else
				{
					maxLagCell = Math.min(maxLag/gridRes,dem.getNumRows()/2);
				}
			}
			else if (direction == Direction.CROSSSTREAM)
			{
				if (downstreamDir==Direction.TOP_TO_BOTTOM)
				{
					maxLagCell = Math.min(maxLag/gridRes,dem.getNumCols()/2);
				}
				else
				{
					maxLagCell = Math.min(maxLag/gridRes,dem.getNumRows()/2);
				}
			}
		}

		// For computational speed, use raster cells as lag units but scale to geographic units for graph.
		float minLagCell = 1;
		Map<Float, Float> data = new TreeMap<Float, Float>(); 
				
		for (float lag=minLagCell; lag<maxLagCell; lag*=1.1)
		{
			InclinationFinder incFinder = new InclinationFinder();

			// Find a set of random sample pairs at each lag.
			for (int sample=0; sample<10000; sample++)
			{
				float z1, z2=0;
				Point p1;
				boolean foundPair = false;

				do
				{
					// Find first non-null random point
					do
					{
						p1 = new Point((int)(Math.random()*dem.getNumCols()),
								(int)(Math.random()*dem.getNumRows()));
						z1 = dem.getAttribute(p1.y, p1.x);
					}
					while (z1 == SpatialModel.NO_VALUE);

					// Find second non-null point either downstream or cross-stream (to the right wrt downstream direction)
					// at the given lag from the first.
					if (((direction == Direction.DOWNSTREAM) && (downstreamDir == Direction.TOP_TO_BOTTOM)) ||
						((direction == Direction.CROSSSTREAM) && (downstreamDir == Direction.LEFT_TO_RIGHT)))
					{
						z2 = dem.getAttribute(Math.round(p1.y + lag),p1.x);
					}
					else
					{
						z2 = dem.getAttribute(p1.y, Math.round(p1.x + lag));
					}
						
					if ((z2 != SpatialModel.NO_VALUE) && (z2 != SpatialModel.OUT_OF_BOUNDS))
					{
						foundPair = true;
					}
				}
				while (foundPair == false);
				
				// Add the pair to those at this lag.
				incFinder.add(z1,z2,lag);
			}
			data.put(new Float(lag),new Float(incFinder.getInclinationIndex()));
		}

		// Pass the inclination index at each lag to the chart.
		float[] xs = new float[data.size()];
		float[] ys = new float[data.size()];
		int n=0;

		for (Float lag : data.keySet())
		{
			xs[n] = lag.floatValue()*gridRes;
			ys[n] = data.get(lag).floatValue();
			n++;
		}

		XYChart newChart = new XYChart(parent);
		newChart.setPointSize(1f);
		newChart.setLineWidth(0.5f);
		newChart.setData(xs, ys);
		newChart.setMinX(1);
		newChart.setMaxX(maxLag);
		newChart.setLogX(true);
		newChart.setXFormat("#");
						
		switch (direction)
		{
			case CROSSSTREAM:
				newChart.setXAxisLabel("Cross-stream Lag ("+units+")");
				break;
			case DOWNSTREAM:
				newChart.setXAxisLabel("Down-stream Lag ("+units+")");
				break;
			default:
				newChart.setXAxisLabel("Lag (all directions, "+units+")");
				break;
		}
		newChart.setYAxisLabel("Inclination index");
		newChart.showXAxis(true);
		newChart.showYAxis(true);
		if (charts.size() > 0)
		{
			newChart.setDecorations(false);
		}		
		charts.add(newChart);

		
		// Ensure all charts have the same scales
		for (XYChart chart : charts)
		{
			maxGlobalLag  = Math.max(maxGlobalLag, chart.getMaxX());
			maxGlobalIncl = Math.max(maxGlobalIncl, Math.max(Math.abs(chart.getMaxY()), Math.abs(chart.getMinY())));
		}
		
		for (XYChart chart : charts)
		{
			chart.setMaxX(maxGlobalLag);
			chart.setMaxY(maxGlobalIncl);
			chart.setMinY(-maxGlobalIncl);
			chart.transposeAxes(false);
		}
	}
}